/* * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.xd.dirt.integration.rabbit; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Properties; import java.util.Set; import org.aopalliance.aop.Advice; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate; import org.springframework.amqp.rabbit.core.ChannelCallback; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.core.support.BatchingStrategy; import org.springframework.amqp.rabbit.core.support.SimpleBatchingStrategy; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer; import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor; import org.springframework.amqp.support.postprocessor.GZipPostProcessor; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.Lifecycle; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.io.Resource; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter; import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint; import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; import org.springframework.integration.channel.DirectChannel; import org.springframework.integration.codec.Codec; import org.springframework.integration.context.IntegrationContextUtils; import org.springframework.integration.endpoint.EventDrivenConsumer; import org.springframework.integration.handler.AbstractMessageHandler; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.SubscribableChannel; import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.xd.dirt.integration.bus.AbstractBusPropertiesAccessor; import org.springframework.xd.dirt.integration.bus.Binding; import org.springframework.xd.dirt.integration.bus.BusProperties; import org.springframework.xd.dirt.integration.bus.BusUtils; import org.springframework.xd.dirt.integration.bus.MessageBus; import org.springframework.xd.dirt.integration.bus.MessageBusSupport; import org.springframework.xd.dirt.integration.bus.MessageValues; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Envelope; /** * A {@link MessageBus} implementation backed by RabbitMQ. * @author Mark Fisher * @author Gary Russell * @author Jennifer Hickey * @author Gunnar Hillert * @author Ilayaperumal Gopinathan * @author David Turanski */ public class RabbitMessageBus extends MessageBusSupport implements DisposableBean { private static final int DEFAULT_LONG_STRING_LIMIT = 8192; private static final AcknowledgeMode DEFAULT_ACKNOWLEDGE_MODE = AcknowledgeMode.AUTO; private static final MessageDeliveryMode DEFAULT_DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT; private static final boolean DEFAULT_DEFAULT_REQUEUE_REJECTED = true; private static final int DEFAULT_MAX_CONCURRENCY = 1; private static final int DEFAULT_PREFETCH_COUNT = 1; private static final String DEFAULT_RABBIT_PREFIX = "xdbus."; private static final int DEFAULT_TX_SIZE = 1; private static final String[] DEFAULT_REQUEST_HEADER_PATTERNS = new String[] { "STANDARD_REQUEST_HEADERS", "*" }; private static final String[] DEFAULT_REPLY_HEADER_PATTERNS = new String[] { "STANDARD_REPLY_HEADERS", "*" }; private static final String DEAD_LETTER_EXCHANGE = "DLX"; private static final Set<Object> RABBIT_CONSUMER_PROPERTIES = new HashSet<Object>(Arrays.asList(new String[] { BusProperties.CONCURRENCY, BusProperties.MAX_CONCURRENCY, RabbitPropertiesAccessor.ACK_MODE, RabbitPropertiesAccessor.PREFETCH, RabbitPropertiesAccessor.PREFIX, RabbitPropertiesAccessor.REQUEST_HEADER_PATTERNS, RabbitPropertiesAccessor.REQUEUE, RabbitPropertiesAccessor.TRANSACTED, RabbitPropertiesAccessor.TX_SIZE, RabbitPropertiesAccessor.AUTO_BIND_DLQ, RabbitPropertiesAccessor.REPUBLISH_TO_DLQ })); /** * Standard + retry + rabbit consumer properties. */ private static final Set<Object> SUPPORTED_BASIC_CONSUMER_PROPERTIES = new SetBuilder() .addAll(CONSUMER_STANDARD_PROPERTIES) .addAll(CONSUMER_RETRY_PROPERTIES) .addAll(RABBIT_CONSUMER_PROPERTIES) .build(); /** * Basic + durable. */ private static final Set<Object> SUPPORTED_PUBSUB_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) .add(BusProperties.DURABLE) .build(); /** * Basic. */ private static final Set<Object> SUPPORTED_NAMED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) .build(); /** * Basic + partitioning. */ private static final Set<Object> SUPPORTED_CONSUMER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) .add(BusProperties.PARTITION_INDEX) .build(); /** * Basic + concurrency + reply headers + delivery mode (reply). */ private static final Set<Object> SUPPORTED_REPLYING_CONSUMER_PROPERTIES = new SetBuilder() // request .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) .add(BusProperties.CONCURRENCY) // reply .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) .add(RabbitPropertiesAccessor.DELIVERY_MODE) .build(); /** * Rabbit producer properties. */ private static final Set<Object> SUPPORTED_BASIC_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_STANDARD_PROPERTIES) .add(RabbitPropertiesAccessor.DELIVERY_MODE) .add(RabbitPropertiesAccessor.PREFIX) .add(RabbitPropertiesAccessor.REQUEST_HEADER_PATTERNS) .add(BusProperties.COMPRESS) .build(); private static final Set<Object> SUPPORTED_PUBSUB_PRODUCER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) .build(); private static final Set<Object> SUPPORTED_NAMED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) .build(); /** * Partitioning + rabbit producer properties. */ private static final Set<Object> SUPPORTED_PRODUCER_PROPERTIES = new SetBuilder() .addAll(PRODUCER_PARTITIONING_PROPERTIES) .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) .add(BusProperties.DIRECT_BINDING_ALLOWED) .addAll(PRODUCER_BATCHING_BASIC_PROPERTIES) .addAll(PRODUCER_BATCHING_ADVANCED_PROPERTIES) .build(); /** * Basic producer + basic consumer + concurrency + reply headers. */ private static final Set<Object> SUPPORTED_REQUESTING_PRODUCER_PROPERTIES = new SetBuilder() // request .addAll(SUPPORTED_BASIC_PRODUCER_PROPERTIES) // reply .addAll(SUPPORTED_BASIC_CONSUMER_PROPERTIES) .add(BusProperties.CONCURRENCY) .add(RabbitPropertiesAccessor.REPLY_HEADER_PATTERNS) .build(); private volatile DefaultMessagePropertiesConverter inboundMessagePropertiesConverter = new DeliveryModeRemovingMessagePropertiesConverter(DEFAULT_LONG_STRING_LIMIT); private static final class DeliveryModeRemovingMessagePropertiesConverter extends DefaultMessagePropertiesConverter { public DeliveryModeRemovingMessagePropertiesConverter(int longStringLimit) { super(longStringLimit); } @Override public MessageProperties toMessageProperties(AMQP.BasicProperties source, Envelope envelope, String charset) { MessageProperties properties = super.toMessageProperties(source, envelope, charset); properties.setDeliveryMode(null); return properties; } }; private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final RabbitAdmin rabbitAdmin; private final RabbitTemplate rabbitTemplate = new RabbitTemplate(); private final GenericApplicationContext autoDeclareContext = new GenericApplicationContext(); private ConnectionFactory connectionFactory; private MessagePostProcessor decompressingPostProcessor = new DelegatingDecompressingPostProcessor(); private MessagePostProcessor compressingPostProcessor = new GZipPostProcessor(); // Default RabbitMQ Container properties private volatile AcknowledgeMode defaultAcknowledgeMode = DEFAULT_ACKNOWLEDGE_MODE; private volatile boolean defaultChannelTransacted; private volatile MessageDeliveryMode defaultDefaultDeliveryMode = DEFAULT_DEFAULT_DELIVERY_MODE; private volatile boolean defaultDefaultRequeueRejected = DEFAULT_DEFAULT_REQUEUE_REJECTED; private volatile int defaultMaxConcurrency = DEFAULT_MAX_CONCURRENCY; private volatile int defaultPrefetchCount = DEFAULT_PREFETCH_COUNT; private volatile int defaultTxSize = DEFAULT_TX_SIZE; private volatile String defaultPrefix = DEFAULT_RABBIT_PREFIX; private volatile String[] defaultRequestHeaderPatterns = DEFAULT_REQUEST_HEADER_PATTERNS; private volatile String[] defaultReplyHeaderPatterns = DEFAULT_REPLY_HEADER_PATTERNS; private volatile boolean defaultAutoBindDLQ = false; private volatile boolean defaultRepublishToDLQ = false; private volatile Integer longStringLimit; private volatile String[] addresses; private volatile String[] adminAddresses; private volatile String[] nodes; private String username; private String password; private String vhost; private boolean useSSL; private Resource sslPropertiesLocation; private String keyStore; private String keyStorePassphrase; private String trustStore; private String trustStorePassphrase; private volatile boolean clustered; public RabbitMessageBus(ConnectionFactory connectionFactory, Codec codec) { Assert.notNull(connectionFactory, "connectionFactory must not be null"); Assert.notNull(codec, "codec must not be null"); this.connectionFactory = connectionFactory; this.rabbitTemplate.setConnectionFactory(connectionFactory); this.rabbitTemplate.afterPropertiesSet(); this.rabbitAdmin = new RabbitAdmin(connectionFactory); this.autoDeclareContext.refresh(); this.rabbitAdmin.setApplicationContext(this.autoDeclareContext); this.rabbitAdmin.afterPropertiesSet(); this.setCodec(codec); } /** * Set a {@link MessagePostProcessor} to decompress messages. Defaults to a * {@link DelegatingDecompressingPostProcessor} with its default delegates. * @param decompressingPostProcessor the post processor. */ public void setDecompressingPostProcessor(MessagePostProcessor decompressingPostProcessor) { this.decompressingPostProcessor = decompressingPostProcessor; } /** * Set a {@link org.springframework.amqp.core.MessagePostProcessor} to compress messages. Defaults to a * {@link org.springframework.amqp.support.postprocessor.GZipPostProcessor}. * @param compressingPostProcessor the post processor. */ public void setCompressingPostProcessor(MessagePostProcessor compressingPostProcessor) { this.compressingPostProcessor = compressingPostProcessor; } public void setDefaultAcknowledgeMode(AcknowledgeMode defaultAcknowledgeMode) { Assert.notNull(defaultAcknowledgeMode, "'defaultAcknowledgeMode' cannot be null"); this.defaultAcknowledgeMode = defaultAcknowledgeMode; } public void setDefaultChannelTransacted(boolean defaultChannelTransacted) { this.defaultChannelTransacted = defaultChannelTransacted; } public void setDefaultDefaultDeliveryMode(MessageDeliveryMode defaultDefaultDeliveryMode) { Assert.notNull(defaultDefaultDeliveryMode, "'defaultDeliveryMode' cannot be null"); this.defaultDefaultDeliveryMode = defaultDefaultDeliveryMode; } public void setDefaultDefaultRequeueRejected(boolean defaultDefaultRequeueRejected) { this.defaultDefaultRequeueRejected = defaultDefaultRequeueRejected; } /** * Set the bus's default max consumers; can be overridden by consumer.maxConcurrency. Values less than 'concurrency' * will be coerced to be equal to concurrency. * @param defaultMaxConcurrency The default max concurrency. */ public void setDefaultMaxConcurrency(int defaultMaxConcurrency) { this.defaultMaxConcurrency = defaultMaxConcurrency; } public void setDefaultPrefetchCount(int defaultPrefetchCount) { this.defaultPrefetchCount = defaultPrefetchCount; } public void setDefaultTxSize(int defaultTxSize) { this.defaultTxSize = defaultTxSize; } public void setDefaultPrefix(String defaultPrefix) { Assert.notNull(defaultPrefix, "'defaultPrefix' cannot be null"); this.defaultPrefix = defaultPrefix.trim(); } public void setDefaultRequestHeaderPatterns(String[] defaultRequestHeaderPatterns) { this.defaultRequestHeaderPatterns = Arrays.copyOf(defaultRequestHeaderPatterns, defaultRequestHeaderPatterns.length); } public void setDefaultReplyHeaderPatterns(String[] defaultReplyHeaderPatterns) { this.defaultReplyHeaderPatterns = Arrays.copyOf(defaultReplyHeaderPatterns, defaultReplyHeaderPatterns.length); } public void setDefaultAutoBindDLQ(boolean defaultAutoBindDLQ) { this.defaultAutoBindDLQ = defaultAutoBindDLQ; } public void setDefaultRepublishToDLQ(boolean defaultRepublishToDLQ) { this.defaultRepublishToDLQ = defaultRepublishToDLQ; } public void setAddresses(String[] addresses) { this.addresses = Arrays.copyOf(addresses, addresses.length); } public void setAdminAddresses(String[] adminAddresses) { this.adminAddresses = Arrays.copyOf(adminAddresses, adminAddresses.length); } public void setNodes(String[] nodes) { this.nodes = Arrays.copyOf(nodes, nodes.length); this.clustered = nodes.length > 1; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setVhost(String vhost) { this.vhost = vhost; } public void setUseSSL(boolean useSSL) { this.useSSL = useSSL; } public void setSslPropertiesLocation(Resource sslPropertiesLocation) { this.sslPropertiesLocation = sslPropertiesLocation; } public void setKeyStore(String keyStore) { this.keyStore = keyStore; } public void setKeyStorePassphrase(String keyStorePassphrase) { this.keyStorePassphrase = keyStorePassphrase; } public void setTrustStore(String trustStore) { this.trustStore = trustStore; } public void setTrustStorePassphrase(String trustStorePassphrase) { this.trustStorePassphrase = this.trustStorePassphrase; } /** * Set the limit for the lengths of LongString headers. Headers greater than * this length are returned as a {@code DataInputStream} which requires user * code to read. Spring AMQP currently does not handle these when converting * back to {@code BasicProperties}. * @param longStringLimit the limit - defaults to 8192. */ public void setLongStringLimit(int longStringLimit) { this.longStringLimit = longStringLimit; } @Override protected void onInit() { super.onInit(); if (this.clustered) { Assert.state(this.addresses.length == this.adminAddresses.length && this.addresses.length == this.nodes.length, "'addresses', 'adminAddresses', and 'nodes' properties must have equal length"); this.connectionFactory = new LocalizedQueueConnectionFactory(this.connectionFactory, this.addresses, this.adminAddresses, this.nodes, this.vhost, this.username, this.password, this.useSSL, this.sslPropertiesLocation, StringUtils.hasText(this.keyStore) ? this.keyStore : null, StringUtils.hasText(this.keyStorePassphrase) ? this.keyStorePassphrase : null, StringUtils.hasText(this.trustStore) ? this.trustStore : null, StringUtils.hasText(this.trustStorePassphrase) ? this.trustStorePassphrase : null); } if (this.longStringLimit != null) { this.inboundMessagePropertiesConverter = new DeliveryModeRemovingMessagePropertiesConverter( this.longStringLimit); } } @Override public void bindConsumer(final String name, MessageChannel moduleInputChannel, Properties properties) { if (logger.isInfoEnabled()) { logger.info("declaring queue for inbound: " + name); } if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { validateConsumerProperties(name, properties, SUPPORTED_NAMED_CONSUMER_PROPERTIES); } else { validateConsumerProperties(name, properties, SUPPORTED_CONSUMER_PROPERTIES); } RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); String queueName = applyPrefix(accessor.getPrefix(this.defaultPrefix), name); String baseQueueName = queueName; int partitionIndex = accessor.getPartitionIndex(); if (partitionIndex >= 0) { queueName += "-" + partitionIndex; } Map<String, Object> args = queueArgs(accessor, queueName); Queue queue = new Queue(queueName, true, false, false, args); declareQueueIfNotPresent(queue); autoBindDLQ(baseQueueName, queueName, accessor); doRegisterConsumer(name, moduleInputChannel, queue, accessor, false); bindExistingProducerDirectlyIfPossible(name, moduleInputChannel); } @Override public void bindPubSubConsumer(String name, MessageChannel moduleInputChannel, Properties properties) { String exchangeName = BusUtils.removeGroupFromPubSub(name); if (logger.isInfoEnabled()) { logger.info("declaring pubsub for inbound: " + name + ", bound to: " + exchangeName); } RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); validateConsumerProperties(name, properties, SUPPORTED_PUBSUB_CONSUMER_PROPERTIES); String prefix = accessor.getPrefix(this.defaultPrefix); FanoutExchange exchange = new FanoutExchange(applyPrefix(prefix, applyPubSub(exchangeName))); declareExchangeIfNotPresent(exchange); Queue queue; boolean durable = accessor.isDurable(this.defaultDurableSubscription); String queueName = applyPrefix(prefix, name); if (durable) { Map<String, Object> args = queueArgs(accessor, queueName); queue = new Queue(queueName, true, false, false, args); } else { queue = new Queue(queueName, false, false, true); } declareQueueIfNotPresent(queue); org.springframework.amqp.core.Binding binding = BindingBuilder.bind(queue).to(exchange); this.rabbitAdmin.declareBinding(binding); // register with context so they will be redeclared after a connection failure if (!this.autoDeclareContext.containsBean(applyPubSub(name))) { this.autoDeclareContext.getBeanFactory().registerSingleton(applyPubSub(name), queue); } String bindingBeanName = exchange.getName() + "." + queue.getName() + ".binding"; if (!this.autoDeclareContext.containsBean(bindingBeanName)) { this.autoDeclareContext.getBeanFactory().registerSingleton(bindingBeanName, binding); } doRegisterConsumer(name, moduleInputChannel, queue, accessor, true); if (durable) { autoBindDLQ(queueName, queueName, accessor); } } private Map<String, Object> queueArgs(RabbitPropertiesAccessor accessor, String queueName) { Map<String, Object> args = new HashMap<>(); if (accessor.getAutoBindDLQ(this.defaultAutoBindDLQ)) { args.put("x-dead-letter-exchange", applyPrefix(accessor.getPrefix(this.defaultPrefix), "DLX")); args.put("x-dead-letter-routing-key", queueName); } return args; } private void doRegisterConsumer(String name, MessageChannel moduleInputChannel, Queue queue, RabbitPropertiesAccessor properties, boolean isPubSub) { // Fix for XD-2503 // Temporarily overrides the thread context classloader with the one where the SimpleMessageListenerContainer // is defined // This allows for the proxying that happens while initializing the SimpleMessageListenerContainer to work // correctly ClassLoader originalClassloader = Thread.currentThread().getContextClassLoader(); try { ClassUtils.overrideThreadContextClassLoader(SimpleMessageListenerContainer.class.getClassLoader()); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer( this.connectionFactory); listenerContainer.setAcknowledgeMode(properties.getAcknowledgeMode(this.defaultAcknowledgeMode)); listenerContainer.setChannelTransacted(properties.getTransacted(this.defaultChannelTransacted)); listenerContainer.setDefaultRequeueRejected(properties.getRequeueRejected(this .defaultDefaultRequeueRejected)); int concurrency = properties.getConcurrency(this.defaultConcurrency); concurrency = concurrency > 0 ? concurrency : 1; listenerContainer.setConcurrentConsumers(concurrency); int maxConcurrency = properties.getMaxConcurrency(this.defaultMaxConcurrency); if (maxConcurrency > concurrency) { listenerContainer.setMaxConcurrentConsumers(maxConcurrency); } listenerContainer.setPrefetchCount(properties.getPrefetchCount(this.defaultPrefetchCount)); listenerContainer.setTxSize(properties.getTxSize(this.defaultTxSize)); listenerContainer.setTaskExecutor(new SimpleAsyncTaskExecutor(queue.getName() + "-")); listenerContainer.setQueues(queue); int maxAttempts = properties.getMaxAttempts(this.defaultMaxAttempts); if (maxAttempts > 1 || properties.getRepublishToDLQ(this.defaultRepublishToDLQ)) { RetryOperationsInterceptor retryInterceptor = RetryInterceptorBuilder.stateless() .maxAttempts(maxAttempts) .backOffOptions(properties.getBackOffInitialInterval(this.defaultBackOffInitialInterval), properties.getBackOffMultiplier(this.defaultBackOffMultiplier), properties.getBackOffMaxInterval(this.defaultBackOffMaxInterval)) .recoverer(determineRecoverer(name, properties)) .build(); listenerContainer.setAdviceChain(new Advice[] { retryInterceptor }); } listenerContainer.setAfterReceivePostProcessors(this.decompressingPostProcessor); listenerContainer.setMessagePropertiesConverter(this.inboundMessagePropertiesConverter); listenerContainer.afterPropertiesSet(); AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(listenerContainer); adapter.setBeanFactory(this.getBeanFactory()); DirectChannel bridgeToModuleChannel = new DirectChannel(); bridgeToModuleChannel.setBeanFactory(this.getBeanFactory()); bridgeToModuleChannel.setBeanName(name + ".bridge"); adapter.setOutputChannel(bridgeToModuleChannel); adapter.setBeanName("inbound." + name); DefaultAmqpHeaderMapper mapper = new DefaultAmqpHeaderMapper(); mapper.setRequestHeaderNames(properties.getRequestHeaderPattens(this.defaultRequestHeaderPatterns)); mapper.setReplyHeaderNames(properties.getReplyHeaderPattens(this.defaultReplyHeaderPatterns)); adapter.setHeaderMapper(mapper); adapter.afterPropertiesSet(); Binding consumerBinding = Binding.forConsumer(name, adapter, moduleInputChannel, properties); addBinding(consumerBinding); ReceivingHandler convertingBridge = new ReceivingHandler(); convertingBridge.setOutputChannel(moduleInputChannel); convertingBridge.setBeanName(name + ".convert.bridge"); convertingBridge.afterPropertiesSet(); bridgeToModuleChannel.subscribe(convertingBridge); consumerBinding.start(); } finally { Thread.currentThread().setContextClassLoader(originalClassloader); } } private MessageRecoverer determineRecoverer(String name, RabbitPropertiesAccessor properties) { if (properties.getRepublishToDLQ(this.defaultRepublishToDLQ)) { RabbitTemplate errorTemplate = new RabbitTemplate(this.connectionFactory); String prefix = properties.getPrefix(this.defaultPrefix); RepublishMessageRecoverer republishMessageRecoverer = new RepublishMessageRecoverer(errorTemplate, deadLetterExchangeName(prefix), applyPrefix(prefix, name)); // TODO: Add container id to republished message headers? (Needs AMQP-489). return republishMessageRecoverer; } else { return new RejectAndDontRequeueRecoverer(); } } @Override public void bindProducer(final String name, MessageChannel moduleOutputChannel, Properties properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); if (name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)) { validateProducerProperties(name, properties, SUPPORTED_NAMED_PRODUCER_PROPERTIES); } else { validateProducerProperties(name, properties, SUPPORTED_PRODUCER_PROPERTIES); } if (!bindNewProducerDirectlyIfPossible(name, (SubscribableChannel) moduleOutputChannel, accessor)) { if (logger.isInfoEnabled()) { logger.info("declaring queue for outbound: " + name); } AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(name, accessor, determineRabbitTemplate(accessor)); doRegisterProducer(name, moduleOutputChannel, queue, accessor); } } private AmqpOutboundEndpoint buildOutboundEndpoint(final String name, RabbitPropertiesAccessor properties, RabbitTemplate rabbitTemplate) { String queueName = applyPrefix(properties.getPrefix(this.defaultPrefix), name); String partitionKeyExtractorClass = properties.getPartitionKeyExtractorClass(); Expression partitionKeyExpression = properties.getPartitionKeyExpression(); AmqpOutboundEndpoint queue = new AmqpOutboundEndpoint(rabbitTemplate); if (partitionKeyExpression == null && !StringUtils.hasText(partitionKeyExtractorClass)) { declareQueueIfNotPresent(new Queue(queueName)); queue.setRoutingKey(queueName); // uses default exchange } else { queue.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression(buildPartitionRoutingExpression (queueName))); // if the stream is partitioned, create one queue for each target partition for (int i = 0; i < properties.getNextModuleCount(); i++) { declareQueueIfNotPresent(new Queue(queueName + "-" + i)); } } configureOutboundHandler(queue, properties); return queue; } private void configureOutboundHandler(AmqpOutboundEndpoint handler, RabbitPropertiesAccessor properties) { DefaultAmqpHeaderMapper mapper = new DefaultAmqpHeaderMapper(); mapper.setRequestHeaderNames(properties.getRequestHeaderPattens(this.defaultRequestHeaderPatterns)); mapper.setReplyHeaderNames(properties.getReplyHeaderPattens(this.defaultReplyHeaderPatterns)); handler.setHeaderMapper(mapper); handler.setDefaultDeliveryMode(properties.getDeliveryMode(this.defaultDefaultDeliveryMode)); handler.setBeanFactory(this.getBeanFactory()); handler.afterPropertiesSet(); } @Override public void bindPubSubProducer(String name, MessageChannel moduleOutputChannel, Properties properties) { validateProducerProperties(name, properties, SUPPORTED_PUBSUB_PRODUCER_PROPERTIES); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); String exchangeName = applyPrefix(accessor.getPrefix(this.defaultPrefix), applyPubSub(name)); declareExchangeIfNotPresent(new FanoutExchange(exchangeName)); AmqpOutboundEndpoint fanout = new AmqpOutboundEndpoint(determineRabbitTemplate(accessor)); fanout.setExchangeName(exchangeName); configureOutboundHandler(fanout, accessor); doRegisterProducer(name, moduleOutputChannel, fanout, accessor); } private RabbitTemplate determineRabbitTemplate(RabbitPropertiesAccessor properties) { RabbitTemplate rabbitTemplate = null; if (properties.isBatchingEnabled(this.defaultBatchingEnabled)) { BatchingStrategy batchingStrategy = new SimpleBatchingStrategy( properties.getBatchSize(this.defaultBatchSize), properties.geteBatchBufferLimit(this.defaultBatchBufferLimit), properties.getBatchTimeout(this.defaultBatchTimeout)); rabbitTemplate = new BatchingRabbitTemplate(batchingStrategy, getApplicationContext().getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class)); rabbitTemplate.setConnectionFactory(this.connectionFactory); } if (properties.isCompress(this.defaultCompress)) { if (rabbitTemplate == null) { rabbitTemplate = new RabbitTemplate(this.connectionFactory); } rabbitTemplate.setBeforePublishPostProcessors(this.compressingPostProcessor); rabbitTemplate.afterPropertiesSet(); } if (rabbitTemplate == null) { rabbitTemplate = this.rabbitTemplate; } return rabbitTemplate; } private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, RabbitPropertiesAccessor properties) { this.doRegisterProducer(name, moduleOutputChannel, delegate, null, properties); } private void doRegisterProducer(final String name, MessageChannel moduleOutputChannel, AmqpOutboundEndpoint delegate, String replyTo, RabbitPropertiesAccessor properties) { Assert.isInstanceOf(SubscribableChannel.class, moduleOutputChannel); MessageHandler handler = new SendingHandler(delegate, replyTo, properties); EventDrivenConsumer consumer = new EventDrivenConsumer((SubscribableChannel) moduleOutputChannel, handler); consumer.setBeanFactory(getBeanFactory()); consumer.setBeanName("outbound." + name); consumer.afterPropertiesSet(); Binding producerBinding = Binding.forProducer(name, moduleOutputChannel, consumer, properties); addBinding(producerBinding); producerBinding.start(); } @Override public void bindRequestor(String name, MessageChannel requests, MessageChannel replies, Properties properties) { if (logger.isInfoEnabled()) { logger.info("binding requestor: " + name); } validateProducerProperties(name, properties, SUPPORTED_REQUESTING_PRODUCER_PROPERTIES); Assert.isInstanceOf(SubscribableChannel.class, requests); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); String queueName = applyRequests(name); AmqpOutboundEndpoint queue = this.buildOutboundEndpoint(queueName, accessor, this.rabbitTemplate); queue.setBeanFactory(this.getBeanFactory()); String replyQueueName = accessor.getPrefix(this.defaultPrefix) + name + ".replies." + this.getIdGenerator().generateId(); this.doRegisterProducer(name, requests, queue, replyQueueName, accessor); Queue replyQueue = new Queue(replyQueueName, false, false, true); // auto-delete declareQueueIfNotPresent(replyQueue); // register with context so it will be redeclared after a connection failure if (!this.autoDeclareContext.containsBean(replyQueueName)) { this.autoDeclareContext.getBeanFactory().registerSingleton(replyQueueName, replyQueue); } this.doRegisterConsumer(name, replies, replyQueue, accessor, false); } @Override public void bindReplier(String name, MessageChannel requests, MessageChannel replies, Properties properties) { if (logger.isInfoEnabled()) { logger.info("binding replier: " + name); } validateConsumerProperties(name, properties, SUPPORTED_REPLYING_CONSUMER_PROPERTIES); RabbitPropertiesAccessor accessor = new RabbitPropertiesAccessor(properties); Queue requestQueue = new Queue(applyPrefix(accessor.getPrefix(this.defaultPrefix), applyRequests(name))); declareQueueIfNotPresent(requestQueue); this.doRegisterConsumer(name, requests, requestQueue, accessor, false); AmqpOutboundEndpoint replyQueue = new AmqpOutboundEndpoint(rabbitTemplate); replyQueue.setExpressionRoutingKey(EXPRESSION_PARSER.parseExpression("headers['" + AmqpHeaders.REPLY_TO + "']")); configureOutboundHandler(replyQueue, accessor); doRegisterProducer(name, replies, replyQueue, accessor); } /** * Try passive declaration first, in case the user has pre-configured the queue with incompatible arguments. * @param queue The queue. */ private void declareQueueIfNotPresent(Queue queue) { if (this.rabbitAdmin.getQueueProperties(queue.getName()) == null) { this.rabbitAdmin.declareQueue(queue); } } /** * Try passive declaration first, in case the user has pre-configured the exchange with incompatible arguments. * @param exchange */ private void declareExchangeIfNotPresent(final Exchange exchange) { this.rabbitTemplate.execute(new ChannelCallback<Void>() { @Override public Void doInRabbit(Channel channel) throws Exception { try { channel.exchangeDeclarePassive(exchange.getName()); } catch (IOException e) { RabbitMessageBus.this.rabbitAdmin.declareExchange(exchange); } return null; } }); } /** * If so requested, declare the DLX/DLQ and bind it. The DLQ is bound to the DLX with a routing key of the original * queue name because we use default exchange routing by queue name for the original message. * @param queueName The base queue name. * @param routingKey the routing key. * @param properties The properties accessor. */ private void autoBindDLQ(final String queueName, String routingKey, RabbitPropertiesAccessor properties) { if (logger.isDebugEnabled()) { logger.debug("autoBindDLQ=" + properties.getAutoBindDLQ(this.defaultAutoBindDLQ) + " for: " + queueName); } if (properties.getAutoBindDLQ(this.defaultAutoBindDLQ)) { String prefix = properties.getPrefix(this.defaultPrefix); String dlqName = constructDLQName(queueName); Queue dlq = new Queue(dlqName); declareQueueIfNotPresent(dlq); final String dlxName = deadLetterExchangeName(prefix); final DirectExchange dlx = new DirectExchange(dlxName); declareExchangeIfNotPresent(dlx); this.rabbitAdmin.declareBinding(BindingBuilder.bind(dlq).to(dlx).with(routingKey)); } } private String deadLetterExchangeName(String prefix) { return prefix + DEAD_LETTER_EXCHANGE; } @Override public void unbindConsumer(String name, MessageChannel channel) { super.unbindConsumer(name, channel); cleanAutoDeclareContext(name); } @Override public void unbindConsumers(String name) { super.unbindConsumers(name); cleanAutoDeclareContext(name); } private void cleanAutoDeclareContext(String name) { if (this.autoDeclareContext.containsBean(applyPubSub(name))) { ConfigurableListableBeanFactory beanFactory = this.autoDeclareContext.getBeanFactory(); if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory).destroySingleton(applyPubSub(name)); } } } @Override public boolean isCapable(Capability capability) { switch (capability) { case DURABLE_PUBSUB: return true; default: return false; } } @Override public void destroy() { stopBindings(); } @Override public void doManualAck(LinkedList<MessageHeaders> messageHeadersList) { Iterator<MessageHeaders> iterator = messageHeadersList.iterator(); Map<Object, Long> channelsToAck = new HashMap<Object, Long>(); while (iterator.hasNext()) { MessageHeaders messageHeaders = iterator.next(); if (messageHeaders.containsKey(AmqpHeaders.CHANNEL)) { Channel channel = (com.rabbitmq.client.Channel) messageHeaders.get(AmqpHeaders.CHANNEL); Long deliveryTag = (Long) messageHeaders.get(AmqpHeaders.DELIVERY_TAG); channelsToAck.put(channel, deliveryTag); } } for (Map.Entry<Object, Long> entry : channelsToAck.entrySet()) { try { ((Channel) entry.getKey()).basicAck(entry.getValue(), true); } catch (IOException e) { logger.error("Exception while manually acknowledging " + e); } } } private class SendingHandler extends AbstractMessageHandler implements Lifecycle { private final MessageHandler delegate; private final String replyTo; private final PartitioningMetadata partitioningMetadata; private SendingHandler(MessageHandler delegate, String replyTo, RabbitPropertiesAccessor properties) { this.delegate = delegate; this.replyTo = replyTo; this.partitioningMetadata = new PartitioningMetadata(properties, properties.getNextModuleCount()); this.setBeanFactory(RabbitMessageBus.this.getBeanFactory()); } @Override protected void handleMessageInternal(Message<?> message) throws Exception { MessageValues messageToSend = serializePayloadIfNecessary(message); if (replyTo != null) { messageToSend.put(AmqpHeaders.REPLY_TO, this.replyTo); } if (this.partitioningMetadata.isPartitionedModule()) { messageToSend.put(PARTITION_HEADER, determinePartition(message, this.partitioningMetadata)); } this.delegate.handleMessage(messageToSend.toMessage(getMessageBuilderFactory())); } @Override public void start() { if (this.delegate instanceof Lifecycle) { ((Lifecycle) this.delegate).start(); } } @Override public void stop() { if (this.delegate instanceof Lifecycle) { ((Lifecycle) this.delegate).stop(); } } @Override public boolean isRunning() { if (this.delegate instanceof Lifecycle) { return ((Lifecycle) this.delegate).isRunning(); } else { return true; } } } private class ReceivingHandler extends AbstractReplyProducingMessageHandler { public ReceivingHandler() { super(); this.setBeanFactory(RabbitMessageBus.this.getBeanFactory()); } @Override protected Object handleRequestMessage(Message<?> requestMessage) { return deserializePayloadIfNecessary(requestMessage).toMessage(getMessageBuilderFactory()); } @Override protected boolean shouldCopyRequestHeaders() { /* * we've already copied the headers so no need for the ARPMH to do it, and we don't want the content-type * restored if absent. */ return false; } } /** * Property accessor for the RabbitMessageBus. Refer to the Spring-AMQP documentation for information on the * specific properties. */ private static class RabbitPropertiesAccessor extends AbstractBusPropertiesAccessor { /** * The acknowledge mode. */ private static final String ACK_MODE = "ackMode"; /** * The delivery mode. */ private static final String DELIVERY_MODE = "deliveryMode"; /** * The prefetch count (basic qos). */ private static final String PREFETCH = "prefetch"; /** * The prefix for queues, exchanges. */ private static final String PREFIX = "prefix"; /** * The reply header patterns. */ private static final String REPLY_HEADER_PATTERNS = "replyHeaderPatterns"; /** * The request header patterns. */ private static final String REQUEST_HEADER_PATTERNS = "requestHeaderPatterns"; /** * Whether delivery failures should be requeued. */ private static final String REQUEUE = "requeue"; /** * Whether to use transacted channels. */ private static final String TRANSACTED = "transacted"; /** * The number of deliveries between acks. */ private static final String TX_SIZE = "txSize"; /** * Whether to automatically declare the DLQ and bind it to the bus DLX. */ private static final String AUTO_BIND_DLQ = "autoBindDLQ"; /** * Whether to automatically declare the DLQ and bind it to the bus DLX. */ private static final String REPUBLISH_TO_DLQ = "republishToDLQ"; public RabbitPropertiesAccessor(Properties properties) { super(properties); } public AcknowledgeMode getAcknowledgeMode(AcknowledgeMode defaultValue) { String ackknowledgeMode = getProperty(ACK_MODE); if (StringUtils.hasText(ackknowledgeMode)) { return AcknowledgeMode.valueOf(ackknowledgeMode); } else { return defaultValue; } } public MessageDeliveryMode getDeliveryMode(MessageDeliveryMode defaultValue) { String deliveryMode = getProperty(DELIVERY_MODE); if (StringUtils.hasText(deliveryMode)) { return MessageDeliveryMode.valueOf(deliveryMode); } else { return defaultValue; } } public int getPrefetchCount(int defaultValue) { return getProperty(PREFETCH, defaultValue); } public String getPrefix(String defaultValue) { return getProperty(PREFIX, defaultValue); } public String[] getReplyHeaderPattens(String[] defaultValue) { return asStringArray(getProperty(REPLY_HEADER_PATTERNS), defaultValue); } public String[] getRequestHeaderPattens(String[] defaultValue) { return asStringArray(getProperty(REQUEST_HEADER_PATTERNS), defaultValue); } public boolean getRequeueRejected(boolean defaultValue) { return getProperty(REQUEUE, defaultValue); } public boolean getTransacted(boolean defaultValue) { return getProperty(TRANSACTED, defaultValue); } public int getTxSize(int defaultValue) { return getProperty(TX_SIZE, defaultValue); } public boolean getAutoBindDLQ(boolean defaultValue) { return getProperty(AUTO_BIND_DLQ, defaultValue); } public boolean getRepublishToDLQ(boolean defaultValue) { return getProperty(REPUBLISH_TO_DLQ, defaultValue); } } }